/**
 * @file Shell_remoto.c
 * @brief Programa cliente
 *
 * Fichero que contiene el programa cliente de shell remota
 * Al ejecutarse, se conecta al servidor definido en la variable
 * de entorno DIR_DESTINO y procede a solicitar comandos al usuario, 
 * que envía al servidor, tras lo que escribe la respuesta del servidor en pantalla
 */

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include "shell_protocols.h"
#include "nivel2.h"

/**
 * Enumeracion con los posibles tipos de peticiones que puede hacer un usuario
 */
typedef enum { CORRECT_REQUEST, MALFORMED_REQUEST, QUIT_REQUEST } User_Req_Type;

/**
 * Tiempo de espera por defecto
 */
#define DEFAULT_TIMEOUT 5000

/**
 * Tiempo de espera definido por el usuario -caso de haberse definido */
int timeout = -1;

/* Macro que calcula el timeout real a esperar */
#define TIMEOUT ((timeout < 0)?DEFAULT_TIMEOUT:timeout)

int user_quit = 0; /*< Flag que comprueba si se debe salir del bucle principal */
int connection_established = 0; /*< Flag que comprueba si hay una conexion establecida */
pid_t main_process; /*< PID del hilo principal. Se usa en callback para sacarlo de espera de IO */
int waiting_for_reply = 0; /*< Flag que marca si estamos aceptando respuestas OK/ERR/DATA */
int waiting_for_data = 0; /*< Flag que marca si estamos aceptando bloques de datos */

/**
 * @brief Funcion de desconexion
 * La funcion client_shutdown finaliza la conexion establecida
 * de haber alguna y a continuacion termina el acceso a las bibliotecas
 * de red
 * Se invoca siempre antes de la finalizacion del programa
 */
void client_shutdown() {
  int ret;
  if(connection_established) {
    printf("Forzando cierre de conexion\n");
    Desconectar();
  }
  printf("Finalizando protocolos de nivel 2\n");
  ret = FinalizarNivel2();
  if(ret == ETH_OK)
    printf("Protocolos de nivel 2 finalizados\n");
  else
    printf("Error al finalizar protocolos de nivel 2\nFin de la ejecucion\n\n");
  
  /* Nuevamente, referencia a 2010 (ver Shell_servidor.h). Esperamos que estas
     referencias no hagan que parezca que no nos tomamos en serio nuestro trabajo. */
  printf("+==========================+\n");
  printf("|Dr. Chandra, will I dream?|\n");
  printf("+==========================+\n");
}

/**
 * @brief Funcion de limpieza de strings
 * @param str String a limpiar -in place-
 */
void chomp(char * str) {
  int n = 0;
  while(str[n] != '\0' && str[n] != '\n')
    n++;

  str[n] = '\0';
}

/**
 * @brief Funcion de impresion con formato de direcciones mac
 * @param mac Mac a imprimir
 */
void print_mac(BYTE* mac) {
  printf("%X:%X:%X:%X:%X:%X", mac[0], mac[1], mac[2],mac[3], mac[4],mac[5]);
}


/**
 * @brief Funcion de manejo de señales
 * La funcion sig_handler sustituye al manejo de las señales SIGINT (para el cerrado del programa) y SIGUSR1
 * SIGUSR1 se lanza desde la funcion de notificacion cuando se recibe un mensaje de desconexion del servidor.
 * Esta desconexion se realiza mediante señales y no mediante flags porque el cliente podría estar bloqueado
 * esperando los comandos del usuario en el momento de recibir la desconexión. Este método hace que la espera
 * acabe de inmediato, finalizando el programa tan pronto como el servidor deja de estar en condiciones para
 * atender mensajes
 * @param signum Codigo de la señal recibida
 */
void sig_handler(int signum) {
  if(signum == SIGINT) {
    printf("Terminacion iniciada por el usuario. Iniciando secuencia de apagado\n");
    client_shutdown();
    exit(1);
  } else if(signum == SIGUSR1) {
    printf("El servidor corto la conexion. Iniciando secuencia de apagado\n");
    client_shutdown();
    exit(1);
  }
}

/**
 * @brief Funcion de notificacion de nivel 2
 * La funcion callback se invoca cada vez que llega un mensaje valido al nivel 2.
 * La misma acepta el mensaje si es un mensaje del tipo que se esperaba (control o datos)
 * o si es una desconexión, e inicia la secuencia de apagado en este ultimo caso
 *
 * @param msg_len Longitud del mensaje recibido en bytes
 * @param msg Puntero a la zona de memoria que contiene el mensaje
 */
int callback(int msg_len, const BYTE * msg) {
  if(msg_len == 0) { /* Desconexion */
    connection_established = 0;
    kill(main_process, SIGUSR1);
  } else if(waiting_for_reply) { /* Esperando respuesta de control */
    if(msg_len == 1 && (msg[0] == REP_OK || msg[0] == REP_ERR)) { /* ERR o OK */
      waiting_for_reply = 0;
      return ETH_OK;
    } else if(msg_len == 1 + sizeof(uint32_t) && msg[0] == REP_DATA) { /* DATA */
      waiting_for_reply = 0;
      waiting_for_data = 1;
      return ETH_OK;
    }
  } else if(waiting_for_data) { /* Esperando datos */
    return ETH_OK;
  }
  return ETH_ERROR;
}

/**
 * @brief Funcion auxiliar que toma la entrada del usuario y la parsea como request
 *
 * La funcion request_from_input convierte una cadena de caracteres escrita por el
 * usuario en una solicitud (caso de ser posible la conversion)
 * @param req Puntero a la estructura Request a cumplimentar
 * @param buffer Puntero a la cadena introducida por el usuario
 * @return QUIT_REQUEST caso de peticion de desconexion, CORRECT_REQUEST caso de peticion correcta de otro tipo, MALFORMED_REQUEST caso de peticion mal formulada -no parse-
 */
User_Req_Type request_from_input(Request * req, char * buffer) {
  if(strcmp(buffer, "quit") == 0) { /* QUIT */
    return QUIT_REQUEST;
  } else if(strncmp(buffer, "pwd", 3) == 0) { /* PWD */
    req->type = REQ_PWD;
    req->parameters = NULL;
    return CORRECT_REQUEST;
  } else if(strncmp(buffer, "cd ", 3) == 0) { /* CD */
    req->type = REQ_CD;
    req->parameters = buffer + 3;
    return CORRECT_REQUEST;
  } else if(strncmp(buffer, "ls", 2) == 0) { /* LS */
    req->type = REQ_LS;
    req->parameters = NULL;
    return CORRECT_REQUEST;
  } else if(strncmp(buffer, "run ", 4) == 0) { /* RUN */
    req->type = REQ_RUN;
    req->parameters = buffer + 4;
    return CORRECT_REQUEST;
  } else {
    return MALFORMED_REQUEST;
  }
}

/**
 * @brief Funcion que envia una peticion al servidor
 *
 * La funcion send_request toma una peticion del cliente y un buffer auxiliar y
 * construye sobre el buffer un mensaje con la peticion, que procede a enviar al
 * servidor
 *
 * @param req Puntero a la solicitud a enviar
 * @param buffer Puntero al buffer auxiliar
 * @return Resultado de la operacion de envio de datos (ETH_OK o ETH_ERROR)
 */
int send_request(Request *req, BYTE * buffer) {
  memcpy(buffer, (BYTE *)(&(req->type)), sizeof(Request_Type)); /* Copiado del tipo */

  switch(req->type) {
  case REQ_CD:
  case REQ_RUN:
    strcpy((char *) buffer+sizeof(Request_Type), req->parameters); /* Copiado de parametros, si procede */
    return EnviarDatos(buffer, sizeof(Request_Type) + strlen(req->parameters) + 1);
    break;
  default:
    return EnviarDatos(buffer, sizeof(Request_Type));
  }
}

/**
 * @brief Funcion que recibe bloques de datos del servidor
 *
 * La funcion get_block_data, invocada despues de recibir un mensaje de control DATA, 
 * se mantiene a la espera de bloques de datos hasta reunir tantos bytes como se le indiquen,
 * que procede a guardar en un buffer dado
 *
 * @param size Cantidad de bytes a recibir
 * @param buffer Puntero al buffer donde se guardaran los datos recibidos (ha de estar reservado de antemano) 
 * @return ETH_OK en caso de recepcion correcta, ETH_ERROR o ETH_TIMEOUT en caso de que la recepcion de un paquete falle
 */
int get_block_data(int size, char * buffer) {
  int total = 0;
  char incoming[256];
  int received_size;

  while(total != size) {
    received_size = RecibirDatos((BYTE*)incoming, TIMEOUT);
    if(received_size == 0 || received_size == ETH_ERROR) 
      return ETH_ERROR;
    if(received_size == ETH_TIMEOUT)
      return ETH_TIMEOUT;
    memcpy(buffer + total, incoming, received_size);
    total += received_size;
  }

  waiting_for_data = 0;
  return ETH_OK;
}

/**
 * @brief Programa principal
 * 
 * Programa principal. Toma un timeout por linea de comandos (o usa uno por defecto)
 * El programa principal entra en un bucle que pide un comando al usuario, lo parsea como
 * request y procede a enviarlo al servidor, para despues imprimir el resultado de su ejecucion
 */
int main(int argc, char ** argv) {
  BYTE server_addr[6]; /* Direccion del servidor */
  BYTE msg_buffer[256]; /* Buffer para mensajes recibidos */
  char buffer[256]; /* Buffer para entrada del usuario */
  Request request; /* Peticion del usuario */
  int received_len; /* Variable que contiene la longitud de los datos recibidos */
  char * data_buffer; /* Buffer para bloques de datos */
  int ret; /* Valor de retorno de funciones */
  uint32_t data_length; /* Longitud del campo de datos de un mensaje */

  /* Lectura del timeout de la linea de comandos */
  if(argc < 2) {
    printf("No se ha especificado un timeout\n");
    printf("El programa usara un timeout por defecto de %d ms\n", TIMEOUT);
  } else if(argc == 2) {
    timeout = strtol(argv[1], &data_buffer, 10);
    if(data_buffer == argv[1] || timeout < 0) {
      timeout = -1;
      printf("El timeout introducido no es valido\n");
      printf("Debe de ser un numero positivo de milisegundos (0 para espera indefinida)\n");
      printf("El programa usara un timeout por defecto de %d ms\n", TIMEOUT);
    } else {
      printf("El programa usara un timeout de %d ms\n", TIMEOUT);
    }
  } else {
    printf("Demasiados argumentos\n");
    printf("Ejecutar con %s [timeout_ms]\n", argv[0]);
    exit(1);
  }
  data_buffer = NULL;
    

  main_process = getpid(); /* Obtencion del PID */

  /* Inicio del nivel 2 */
  printf("Iniciando nivel 2\n");
  if(IniciarNivel2(callback) == ETH_ERROR) {
    printf("Imposible iniciar nivel 2\n");
    return -1;
  }
  printf("Nivel 2 iniciado\n\n");

  /* Captura de señales */
  signal(SIGINT, sig_handler);
  signal(SIGUSR1, sig_handler);

  /* Obtencion de la direccion de destino */
  if(ObtenerDirDestino(server_addr) != ETH_OK) {
    printf("Imposible obtener direccion de destino\n");
    client_shutdown();
    exit(1);
  }

  printf("Solicitando conexion con servidor ");
  print_mac(server_addr);
  printf("\n");
  /* Solicitud de la conexion */
  if(SolicitarConexion(server_addr) == ETH_ERROR) {
    printf("Incapaz de establecer conexion con el servidor\n");
  } else {
    connection_established = 1;
    printf("Conexion establecida\n\n");

    while(!user_quit) { /* Bucle principal */
      printf("\n");
      fgets(buffer, 256, stdin); /* Lectura de comando por stdin */
      chomp(buffer); 
      switch(request_from_input(&request, buffer)) {  /* Parsing */
      case QUIT_REQUEST: /* Desconexion */
	printf("El usuario solicito la desconexion\n");
	user_quit = 1;
	break;
      case CORRECT_REQUEST: /* Peticion valida */
	waiting_for_reply = 1;
	if((ret = send_request(&request, msg_buffer)) == ETH_ERROR) { /* Envio de peticion */ /* ETH_ERROR */
	  printf("Error al enviar comando\n");
	  printf("Conexion interrumpida\n");
	  client_shutdown();
	  exit(1);
	} else if(ret == ETH_TIMEOUT) { /* ETH_TIMEOUT -nunca se entra, por si acaso- */
	  printf("Tiempo de espera agotado al enviar comando");
	} else { /* ETH_OK */
	  received_len = RecibirDatos(msg_buffer, TIMEOUT); /* Lectura de respuesta del servidor */
	  switch(received_len) {
	  case ETH_ERROR: /* Recepcion de mensaje erronea */
	    printf("Error al recibir datos del servidor\n");
	    break;
	  case ETH_TIMEOUT: /* Exceso de retraso */
	    printf("El servidor esta tardando demasiado en responder\n");
	    break;
	  default: /* Recepcion correcta */
	    if(msg_buffer[0] == REP_ERR) { /* El servidor notifica un error */
	      printf("Error en la ejecucion del comando\n");

	    } else if(msg_buffer[0] == REP_OK) { /* El servidor notifica ejecucion exitosa */
	      printf("Comando ejecutado con exito\n");

	    } else if(msg_buffer[0] == REP_DATA) { /* El servidor notifica el envio de datos */
	      data_length = *((uint32_t*)(msg_buffer + 1));
	      printf("Respuesta del servidor\n");
	      printf("Bloque de datos de longitud %d\n\n", data_length);
	      if((data_buffer = calloc(data_length, sizeof(char))) == NULL) { /* Reserva de memoria para datos entrantes */
		printf("Error al reservar memoria!\n");
		printf("El programa se cerrara\n");
		client_shutdown();
		exit(1);
	      }
	      if((ret = get_block_data(data_length, data_buffer)) == ETH_ERROR) { /* Lectura de bloques de datos de respuesta */
		printf("Error al recibir datos del servidor\n");
	      } else if(ret == ETH_TIMEOUT) {
		printf("El servidor ha tardado demasiado en responder\n");
	      } else {
		printf("%s", data_buffer);
		printf("\n");
	      }
	      free(data_buffer);
	    }
	  }
	}
	break;
      case MALFORMED_REQUEST: /* Error: Comando no reconocido */
	printf("Comando no reconocido\n");
	printf("Comandos validos:\n");
	printf("\tquit\n");
	printf("\tls\n");
	printf("\tpwd\n");
	printf("\tcd new_dir\n");
	printf("\trun prog_name\n");
	printf("\n");
	break;
      }
    }
  }
    
  
  client_shutdown();
  
  return 0;
}
